﻿using System;
using System.Linq;
using System.Reactive.Disposables;
using System.Reactive.Linq;
using System.Reflection;
using System.Threading;
using nRoute.Components.Collections;
using nRoute.Components.Composition.Adapters;
using nRoute.Components.Messaging;
using nRoute.Mapping;
using nRoute.Utilities;
#if (!SILVERLIGHT)
using System.Collections.Concurrent;
using System.Threading.Tasks;
#elif (WINRT)
using Windows.UI.Xaml;
using Windows.UI.Core;
using System.Threading.Tasks;
using System.Reflection;
#endif

namespace nRoute.Components.Composition
{
    public partial class RuntimeMapper
        : IDisposable
    {
        private const string CANNOT_RESOLVE_RESOURCE = "Cannot resolve resource of type {0}";
        private const string CANNOT_RESOLVE_LOCATOR = "Cannot resolve locator for type {0}";
        private const string CANNOT_RESOLVE_ON = "Could not resolve resource type on {0}";
        private const string CANNOT_RESOLVE_IRESOURCELOCATOR = "Could not resolve IResourceLocator on {0}";
        private const string COULDNOT_CONSITUTE_SYSTEM_SERVICE = "Could not constitute system service of type {0}";
        private const string COULDNOT_INITIALIZE_SYSTEM_SERVICE = "Could not initialize system service of type {0}";

        private readonly ConcurrentOrderedList<PendingMapping> _pendingMappings;
        private readonly ChannelObserver<MappedAssemblyInfo> _assemblyObserver;
        private readonly IResourceCatalogEx _resourceCatalog;
        private int _refCount = 1;

#if (WINRT)
        private TaskCompletionSource<bool> _taskCoordinator;
#else
        private Action _completionCallback;
#endif

        public RuntimeMapper(IResourceCatalogEx resourceCatalog)
        {
            Guard.ArgumentNotNull(resourceCatalog, "resourceCatalog");
            _resourceCatalog = resourceCatalog;

            _pendingMappings = new ConcurrentOrderedList<PendingMapping>();
            _assemblyObserver = new ChannelObserver<MappedAssemblyInfo>(OnAssemblyMapped);
        }

#if (WINRT)
        public async Task Initialize()
        {
            _taskCoordinator = new TaskCompletionSource<bool>();

            await AssemblyMapper.GetApplicationAssemblies();
            MapSystemAssemblies();

            await _taskCoordinator.Task;
            _taskCoordinator = null;
        }
#else
        public void Initialize(Action completetionCallback)
        {
            _completionCallback = completetionCallback;

            AssemblyMapper.GetApplicationAssemblies();
            MapSystemAssemblies();
        }
#endif
        
#region Mapping Realted

        private void MapSystemAssemblies()
        {
            var _assemblies = AssemblyMapper.MappedAssemblies.ToArray();
            var _pendingServices = new ConcurrentOrderedList<ISystemService>();

            foreach (var _assembly in _assemblies)
            {
                MapSystemAssembly(_assembly, _pendingServices);
            }

            // resolve all services
            if (_pendingServices.Count > 0)
            {
                var _resourceMappedFlag = false;
                while (true)
                {
                    var _pendingServicesInstance = _pendingServices.ToArray();
                    foreach (var _pendingService in _pendingServicesInstance)
                    {
                        if (_pendingService.CanInitialize(_resourceCatalog))
                        {
                            Interlocked.Increment(ref _refCount);
                            _pendingService.Initialize(_resourceCatalog, SystemServiceInitialized);

                            _pendingServices.TryRemove(_pendingService);
                            _resourceMappedFlag = true;
                        }
                    }

                    // if there was no service initialized in this loop then throw an error as long as we don't have any pending service to initialize
                    if (!_resourceMappedFlag)
                    {
                        if (_pendingServices.Count > 0)
                        {
                            throw new InvalidOperationException(string.Format(COULDNOT_INITIALIZE_SYSTEM_SERVICE, _pendingServices.First().GetType()));
                        }
                        else
                        {
                            break;
                        }
                    }
                    _resourceMappedFlag = false;
                }
            }

            var _balCount = Interlocked.Decrement(ref _refCount);
            if (_balCount == 0) MapAssemblies();
        }

        private void MapSystemAssembly(Assembly assembly, ConcurrentOrderedList<ISystemService> pendingServices)
        {
            AssemblyMapper.GetAttributesInMappedAssembly<MapAsSystemResourceAttribute>(assembly, (a, t) =>
            {
                var _service = _resourceCatalog.TypeBuilder.BuildType(t) as ISystemService;
                if (_service == null)
                {
                    throw new InvalidOperationException(string.Format(COULDNOT_CONSITUTE_SYSTEM_SERVICE, t.FullName));
                }

                if (_service.CanInitialize(_resourceCatalog))
                {
                    Interlocked.Increment(ref _refCount);
                    _service.Initialize(_resourceCatalog, SystemServiceInitialized);
                }
                else
                {
                    pendingServices.TryAdd(_service);
                }
            });
        }

        private void SystemServiceInitialized(Type type, IResourceLocator resourceLocator)
        {
            Guard.ArgumentNotNull(type, "type");
            Guard.ArgumentNotNull(resourceLocator, "resourceLocator");

            // add to catalog
            _resourceCatalog.CatalogResource(type, type.FullName, resourceLocator, true);
            
            // once finished map normal resources
            var _balCount = Interlocked.Decrement(ref _refCount);
            if (_balCount == 0)
            {
                MapAssemblies();
            }
        }

        private void MapAssemblies()
        {
            var _assemblies = AssemblyMapper.MappedAssemblies.ToArray();
            foreach (var _assembly in _assemblies)
            {
                MapAssembly(_assembly);
            }

            // followed by adapters
            MapKnownAdapters();
        }

        private void MapAssembly(Assembly assembly)
        {
            // we first look for given known type of resources
            AssemblyMapper.GetAttributesInMappedAssembly<MapAsKnownResourceAttribute>(assembly, (a, t) =>
            {
                var _knownType = a.GetKnownResourceType(t);
                _resourceCatalog.CatalogAsKnownType(_knownType);
            });

            // we map every attribute - note_, a is the attribute instance and t is type it is applied on
            AssemblyMapper.GetAttributesInMappedAssembly<MapResourceBaseAttribute>(assembly, (a, t) =>
            {
                if (!ProcessMappingAttribute(a, t))
                {
                    _pendingMappings.TryAdd(new PendingMapping(a, t));
                }
            });

            // we check if any there are any pending items and as to if they can be resolved
            // note we do this recursively, as there might be circular dependencies
            if (_pendingMappings.Count > 0)
            {
                var _resourceMappedFlag = false;
                while (true)
                {
                    var _pendingMappingsInstance = _pendingMappings.ToArray();
                    foreach (var _pendingMapping in _pendingMappingsInstance)
                    {
                        // if it can 
                        if (ProcessMappingAttribute(_pendingMapping.MappingAttribute, _pendingMapping.TargetType))
                        {
                            _pendingMappings.TryRemove(_pendingMapping);
                            _resourceMappedFlag = true;
                        }
                    }

                    // if not even one entity was resolved then we return
                    if (!_resourceMappedFlag) break;
                    _resourceMappedFlag = false;
                }
            }
        }

        private bool ProcessMappingAttribute(MapResourceBaseAttribute attribute, Type targetType)
        {
            var _resourceAttribute = (MapResourceBaseAttribute)attribute;
            var _resourceType = _resourceAttribute.GetResourceType(targetType);     // note resource type can be different

            if (_resourceType == null)
            {
                throw new InvalidOperationException(string.Format(CANNOT_RESOLVE_ON, targetType.FullName));
            }

            if (_resourceAttribute.CanInitialize(targetType, _resourceCatalog))
            {
                var _resourceLocator = _resourceAttribute.GetResourceLocator(targetType, _resourceCatalog.TypeBuilder);
                ProcessResourceMeta(targetType, _resourceType, _resourceAttribute.Key, _resourceLocator);

                if (_resourceCatalog.CatalogResource(_resourceType, _resourceAttribute.Key, _resourceLocator, _resourceAttribute.IsDefaultResource))
                {
                    return true;
                }
            }
        
            return false;
        }

        private void ProcessResourceMeta(Type targetType, Type resourceType, string key, IResourceLocator resourceLocator)
        {
#if (WINRT)
            var _metaAttributes = targetType.GetTypeInfo().GetCustomAttributes<ResourceMetaAttributeBase>(true).ToArray();
#else
            var _metaAttributes = Attribute.GetCustomAttributes(targetType, typeof(ResourceMetaAttributeBase), true);
#endif
            if (_metaAttributes == null || _metaAttributes.Length == 0) return;

            foreach (ResourceMetaAttributeBase _metaAttribute in _metaAttributes)
            {
                var _resourceType = _metaAttribute.GetResourceType(targetType);
                if (_resourceType == null || _resourceType == resourceType)
                {
                    _metaAttribute.AppendMeta(resourceType, key, resourceLocator.Metadata);
                }
            }
        }

        private void MapKnownAdapters()
        {
            // and register built-in adapters
           KnownAdapters.RegisterKnownAdapters(_resourceCatalog);

            // and finally
#if (WINRT)
            if (_taskCoordinator != null)
            {
                _taskCoordinator.SetResult(true);
            }
#else
            if (_completionCallback != null)
            {
                _completionCallback();
                _completionCallback = null;
            }
#endif
        }

#endregion

#region Public Methods

        public void ThrowIfHasPendingMapping()
        {
            var _pendingMappingsInstance = _pendingMappings.ToArray();
            foreach (var _pendingMapping in _pendingMappingsInstance)
            {
                throw new UnresolvedMappingsException(_pendingMapping.TargetType, _pendingMapping.MappingAttribute.GetType());
            }
        }

#endregion

#region Helpers

        private void OnAssemblyMapped(MappedAssemblyInfo info)
        {
            if (info == null || info.Assembly == null) return;
            MapAssembly(info.Assembly);
        }
        
        protected virtual void Dispose(bool disposing) 
        {
            if (disposing)
            {
                _assemblyObserver.Dispose();
                _pendingMappings.Clear();
            }
        }

#endregion

#region IDisposable Members

        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }

#endregion

#region Nested Classes

        private class PendingMapping
        {
            private readonly Type _targetType;
            private MapResourceBaseAttribute _mappingAttribute;

            public PendingMapping(MapResourceBaseAttribute mappingAttribute, Type targetType)
            {
                _mappingAttribute = mappingAttribute;
                _targetType = targetType;
            }

            public MapResourceBaseAttribute MappingAttribute
            {
                get { return _mappingAttribute; }
            }

            public Type TargetType
            {
                get { return _targetType; }
            }
        }

#endregion

        ~RuntimeMapper() { Dispose(false); }

    }
}